/* Copyright (c) 2010, Michael J. Allison

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice, 
 this list of conditions and the following disclaimer.
 
 * Redistributions in binary form must reproduce the above copyright notice, 
 this list of conditions and the following disclaimer in the documentation 
 and/or other materials provided with the distribution.
 
 * Neither the name of Michael J. Allison nor the names of contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
 THE POSSIBILITY OF SUCH DAMAGE.
 */

package xamcommons.objectinspector;

import java.io.File;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import javax.swing.JOptionPane;

import xamcommons.objectinspector.CanonicalField.FieldStatus;

import org.snia.xam.XAMLibrary;

/**
 * A collection of all canonical fields to be used for comparison when 
 * displaying objects. This class allows the caller to request a field 
 * check for compliance with defined readOnly/binding/type values via the 
 * <i>checkField</i> method.
 */
public class CanonicalClass
{
   /** Pattern codes. */
   final static String CODE_EXACT = "E";
   final static String CODE_PATTERN = "P";
   
   /** Used in creation of class list. */
   final static String CannonicalLock = "CanonLock";
   
   static Hashtable<String,CanonicalClass> m_classList;
   
   private String m_name;
   Hashtable<String,CanonicalField> m_exactFields;
   Hashtable<String,CanonicalField> m_patternFields;
   
   private boolean m_loadedCorrectly = false;
   
   /**
    * Build an empty CanonicalClass with no field definitions. 
    * @param className
    */
   public CanonicalClass( String className )
   {
      m_name = className;
      m_exactFields = new Hashtable<String,CanonicalField>();
      m_patternFields = new Hashtable<String,CanonicalField>();
   }
   
   /**
    * Load field definitions from the named resource. The resource is a text
    * file with CanonicalField definition lines in it. A line may consist of 
    * a comment, begining with a '#' character or a definition. A definition
    * consists of a pattern type descriptor (either 'E' or 'P'), field name 
    * pattern, readOnly setting, binding setting, and MIME content type. 
    * 
    * FIELD_DEFINITION ::= COMMENT | FIELD_DESCRIPTION
    * COMMENT ::= '#' CHAR*
    * FIELD_DESCRIPTION ::= PATTERN_TYPE PATTERN READONLY BINDING MIME_TYPE
    * PATTERN_TYPE ::= 'E' | 'P'
    * PATTERN ::= <i>java regex expression</i>
    * READONLY ::= BOOLEAN
    * BINDING ::= BOOLEAN
    * BOOLEAN :== 'true' | 'false'
    * 
    * Examples:
    * E:.xam.apiLevel:true:false:application/vnd.snia.xam.string
    * <i>Checks for an exact match of field '.xam.apiLevel'</i>
    *
    * P:.xsystem.access.policy.list.*:true:false:application/vnd.snia.xam.string
    * <i>Checks for any field beginning with '.xsystem.access.policy.list.'
    *
    * <b>Bugs:</b>
    * For fields containing Java Regex meta characters, the meta characters 
    * need to be properly escaped to prevent Regex confusion. 
    *
    * @param filename Usually the classname
    * @throws IOException 
    */
   public void load( String filename )
   {
      int lineNo = 0;
      try
      {
         InputStream resource = this.getClass().getResourceAsStream( "/"+filename );
         if( resource == null )
            return;
         BufferedReader reader = new BufferedReader( new InputStreamReader(
                                                        resource  ));
         String line;
         
         while ((line = reader.readLine()) != null)
         {
            lineNo++;
            
            line = line.trim();
            if( (line.length() == 0) || (line.charAt(0) == '#') )
               continue;
            
            StringTokenizer tok = new StringTokenizer(line, ":");
            String code = (String) tok.nextElement();
            String name = (String) tok.nextElement();
            String roStr = (String) tok.nextElement();
            boolean readOnly = Boolean.valueOf(roStr);
            String bindStr = (String) tok.nextElement();
            boolean binding = Boolean.valueOf(bindStr);
            String type = (String) tok.nextElement();
            CanonicalField field = new CanonicalField(
               CanonicalField.Requirement.REQUIRED, name, readOnly, binding,
               type);
            if( code.equals( CODE_EXACT ) )
               m_exactFields.put(field.getName(), field);
            else
               m_patternFields.put(field.getName(), field );
         }
      }
      catch (Exception e)
      {
         JOptionPane.showMessageDialog(null,
                                       "Error on line number " + lineNo + " of field definitions " + filename + ": " + e);
      }
      m_loadedCorrectly = true;
   }
   
   /**
    * Returns true if the class loaded the canonical field descriptions 
    * correctly, otherwise return false.
    */
   public boolean isLoadedCorrectly() { return m_loadedCorrectly; } 
   
   /**
    * Checks a field against a definition. If the field is in the explicit 
    * definitions, use that, otherwise find one based on a search through 
    * the pattern (regex) expressions. The actual field value must match 
    * readOnly, binding, and type to pass the check, returning true otherwise
    * returning false.
    * @param name The actual field name
    * @param readOnly The readOnly setting of the field
    * @param binding The binding setting of the field
    * @param type The full MIME content type of the field
    */
   public FieldStatus checkField( String  name, 
                                  boolean readOnly, 
                                  boolean binding, 
                                  String  type )
   {
      CanonicalField field = m_exactFields.get( name );
      if( field != null )
      {
         if( name.equals(field.getName()) &&
             (readOnly == field.getReadOnly()) &&
             (binding  == field.getBinding()) &&
             type.equals(field.getType()) )
            return FieldStatus.OK;
         else
            return FieldStatus.ERROR;
      }
      
      // Check for the patterned fields. 
      Enumeration<String> e = m_patternFields.keys();
      while( e.hasMoreElements() )
      {
         String key = e.nextElement();
         field = m_patternFields.get(key);
         Pattern pattern = Pattern.compile( key );
         Matcher matcher = pattern.matcher(name);
         if( matcher.find() )
         {
            if( matcher.start() != 0 )
               continue;
            
            if( (readOnly == field.getReadOnly()) &&
                (binding  == field.getBinding()) &&
                type.equals(field.getType()) )
               return FieldStatus.OK;
            else
               return FieldStatus.ERROR;
         }
      }
      return FieldStatus.EXTRA;
   }
   
   /**
    * Get the canonical class for the named FieldContainer
    * @param className
    * @return
    */
   static public CanonicalClass GetClass( String className )
   {
      synchronized( CannonicalLock )
      {
         if( m_classList == null )
            m_classList = new Hashtable<String, CanonicalClass>();
         
         CanonicalClass canonClass = m_classList.get(className);
         
         if( canonClass == null )
         {
            canonClass = new CanonicalClass( className );
            canonClass.load( className );
            m_classList.put( className, canonClass );
         }
         return canonClass;
      } 
   }
}
